[id].vue 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. <template>
  2. <div class="admin--news-form">
  3. <div v-if="isLoading" class="admin--loading">데이터를 불러오는 중...</div>
  4. <form v-else @submit.prevent="handleSubmit" class="admin--form">
  5. <!-- 댓글허용 -->
  6. <div class="admin--form-group">
  7. <label class="admin--form-label">댓글허용</label>
  8. <div class="admin--checkbox-group">
  9. <label class="admin--checkbox-label">
  10. <input v-model="formData.allow_comment" type="checkbox" />
  11. <span>댓글 허용</span>
  12. </label>
  13. </div>
  14. </div>
  15. <!-- 공지 -->
  16. <div class="admin--form-group">
  17. <label class="admin--form-label">공지</label>
  18. <div class="admin--checkbox-group">
  19. <label class="admin--checkbox-label">
  20. <input v-model="formData.is_notice" type="checkbox" />
  21. <span>공지글로 등록</span>
  22. </label>
  23. </div>
  24. </div>
  25. <!-- 이름 -->
  26. <div class="admin--form-group">
  27. <label class="admin--form-label"
  28. >이름 <span class="admin--required">*</span></label
  29. >
  30. <input
  31. v-model="formData.name"
  32. type="text"
  33. class="admin--form-input"
  34. placeholder="이름을 입력하세요"
  35. required
  36. />
  37. </div>
  38. <!-- 이메일 -->
  39. <div class="admin--form-group">
  40. <label class="admin--form-label"
  41. >이메일 <span class="admin--required">*</span></label
  42. >
  43. <input
  44. v-model="formData.email"
  45. type="email"
  46. class="admin--form-input"
  47. placeholder="이메일을 입력하세요"
  48. required
  49. />
  50. </div>
  51. <!-- URL -->
  52. <div class="admin--form-group">
  53. <label class="admin--form-label">URL</label>
  54. <input
  55. v-model="formData.url"
  56. type="url"
  57. class="admin--form-input"
  58. placeholder="https://example.com"
  59. />
  60. </div>
  61. <!-- 제목 -->
  62. <div class="admin--form-group">
  63. <label class="admin--form-label"
  64. >제목 <span class="admin--required">*</span></label
  65. >
  66. <input
  67. v-model="formData.title"
  68. type="text"
  69. class="admin--form-input"
  70. placeholder="제목을 입력하세요"
  71. required
  72. />
  73. </div>
  74. <!-- 내용 -->
  75. <div class="admin--form-group">
  76. <label class="admin--form-label"
  77. >내용 <span class="admin--required">*</span></label
  78. >
  79. <SunEditor v-model="formData.content" />
  80. </div>
  81. <!-- 기존 첨부파일 -->
  82. <div v-if="existingFiles.length > 0" class="admin--form-group">
  83. <label class="admin--form-label">기존 첨부파일</label>
  84. <div class="admin--file-list">
  85. <div
  86. v-for="(file, index) in existingFiles"
  87. :key="'existing-' + index"
  88. class="admin--file-item"
  89. >
  90. <a :href="file.url" target="_blank" class="admin--file-name">{{
  91. file.name
  92. }}</a>
  93. <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
  94. <button
  95. type="button"
  96. class="admin--btn-remove-file"
  97. @click="removeExistingFile(index)"
  98. >
  99. 삭제
  100. </button>
  101. </div>
  102. </div>
  103. </div>
  104. <!-- 새 파일첨부 -->
  105. <div class="admin--form-group">
  106. <label class="admin--form-label">파일첨부</label>
  107. <div v-if="attachedFiles.length > 0" class="admin--file-list">
  108. <div
  109. v-for="(file, index) in attachedFiles"
  110. :key="'new-' + index"
  111. class="admin--file-item"
  112. >
  113. <span class="admin--file-name">{{ file.name }}</span>
  114. <span class="admin--file-size">({{ formatFileSize(file.size) }})</span>
  115. <button
  116. type="button"
  117. class="admin--btn-remove-file"
  118. @click="removeFile(index)"
  119. >
  120. 삭제
  121. </button>
  122. </div>
  123. </div>
  124. <input
  125. ref="fileInput"
  126. type="file"
  127. multiple
  128. class="admin--form-file-hidden"
  129. @change="handleFileAdd"
  130. />
  131. <button
  132. type="button"
  133. class="admin--btn admin--btn-secondary"
  134. @click="triggerFileInput"
  135. >
  136. 파일 추가
  137. </button>
  138. </div>
  139. <!-- 버튼 영역 -->
  140. <div class="admin--form-actions">
  141. <button type="submit" class="admin--btn admin--btn-primary" :disabled="isSaving">
  142. {{ isSaving ? "저장 중..." : "확인" }}
  143. </button>
  144. <button type="button" class="admin--btn admin--btn-secondary" @click="goToList">
  145. 목록
  146. </button>
  147. </div>
  148. <!-- 성공/에러 메시지 -->
  149. <div v-if="successMessage" class="admin--alert admin--alert-success">
  150. {{ successMessage }}
  151. </div>
  152. <div v-if="errorMessage" class="admin--alert admin--alert-error">
  153. {{ errorMessage }}
  154. </div>
  155. </form>
  156. </div>
  157. </template>
  158. <script setup>
  159. import { ref, onMounted } from "vue";
  160. import { useRoute, useRouter } from "vue-router";
  161. import SunEditor from "~/components/admin/SunEditor.vue";
  162. definePageMeta({
  163. layout: "admin",
  164. middleware: ["auth"],
  165. });
  166. const route = useRoute();
  167. const router = useRouter();
  168. const { get, put, upload } = useApi();
  169. const isLoading = ref(true);
  170. const isSaving = ref(false);
  171. const successMessage = ref("");
  172. const errorMessage = ref("");
  173. const attachedFiles = ref([]);
  174. const existingFiles = ref([]);
  175. const fileInput = ref(null);
  176. const formData = ref({
  177. allow_comment: false,
  178. is_notice: false,
  179. name: "",
  180. email: "",
  181. url: "",
  182. title: "",
  183. content: "",
  184. file_urls: [],
  185. });
  186. const loadNews = async () => {
  187. isLoading.value = true;
  188. const id = route.params.id;
  189. const { data, error } = await get(`/board/notice/${id}`);
  190. console.log("[NewsEdit] 데이터 로드:", { data, error });
  191. if (data?.success && data?.data) {
  192. const news = data.data;
  193. formData.value = {
  194. allow_comment: news.allow_comment === 1 || news.allow_comment === '1',
  195. is_notice: news.is_notice === 1 || news.is_notice === '1',
  196. name: news.name || "",
  197. email: news.email || "",
  198. url: news.url || "",
  199. title: news.title || "",
  200. content: news.content || "",
  201. file_urls: news.file_urls || [],
  202. };
  203. existingFiles.value = news.file_urls || [];
  204. console.log("[NewsEdit] 로드 성공");
  205. }
  206. isLoading.value = false;
  207. };
  208. const triggerFileInput = () => {
  209. fileInput.value?.click();
  210. };
  211. const handleFileAdd = (event) => {
  212. const files = Array.from(event.target.files);
  213. attachedFiles.value.push(...files);
  214. event.target.value = "";
  215. };
  216. const removeFile = (index) => {
  217. attachedFiles.value.splice(index, 1);
  218. };
  219. const removeExistingFile = (index) => {
  220. existingFiles.value.splice(index, 1);
  221. };
  222. const formatFileSize = (bytes) => {
  223. if (bytes === 0) return "0 Bytes";
  224. const k = 1024;
  225. const sizes = ["Bytes", "KB", "MB", "GB"];
  226. const i = Math.floor(Math.log(bytes) / Math.log(k));
  227. return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
  228. };
  229. const handleSubmit = async () => {
  230. successMessage.value = "";
  231. errorMessage.value = "";
  232. if (!formData.value.title) {
  233. errorMessage.value = "제목을 입력하세요.";
  234. return;
  235. }
  236. if (!formData.value.content) {
  237. errorMessage.value = "내용을 입력하세요.";
  238. return;
  239. }
  240. isSaving.value = true;
  241. try {
  242. let fileUrls = [...existingFiles.value];
  243. // 새 파일 업로드
  244. if (attachedFiles.value.length > 0) {
  245. for (const file of attachedFiles.value) {
  246. const formDataFile = new FormData();
  247. formDataFile.append("file", file);
  248. const { data: uploadData, error: uploadError } = await upload(
  249. "/upload/news-file",
  250. formDataFile
  251. );
  252. if (uploadError) {
  253. errorMessage.value = `파일 업로드에 실패했습니다: ${file.name}`;
  254. isSaving.value = false;
  255. return;
  256. }
  257. fileUrls.push({
  258. name: file.name,
  259. url: uploadData.data.url,
  260. size: file.size,
  261. });
  262. }
  263. }
  264. // content에서 도메인 제거
  265. let contentToSave = formData.value.content;
  266. if (contentToSave) {
  267. // http://도메인 또는 https://도메인 제거
  268. contentToSave = contentToSave.replace(/https?:\/\/[^\/]+/g, "");
  269. }
  270. const submitData = {
  271. ...formData.value,
  272. allow_comment: formData.value.allow_comment ? 1 : 0,
  273. is_notice: formData.value.is_notice ? 1 : 0,
  274. content: contentToSave,
  275. file_urls: fileUrls,
  276. };
  277. const id = route.params.id;
  278. const { data, error } = await put(`/board/notice/${id}`, submitData);
  279. if (error) {
  280. errorMessage.value = error.message || "수정에 실패했습니다.";
  281. } else {
  282. successMessage.value = "뉴스가 수정되었습니다.";
  283. setTimeout(() => {
  284. router.push("/site-manager/board/notice");
  285. }, 1000);
  286. }
  287. } catch (error) {
  288. errorMessage.value = "서버 오류가 발생했습니다.";
  289. console.error("Save error:", error);
  290. } finally {
  291. isSaving.value = false;
  292. }
  293. };
  294. const goToList = () => {
  295. router.push("/site-manager/board/notice");
  296. };
  297. onMounted(() => {
  298. loadNews();
  299. });
  300. </script>